/*
 * MIT License
 *
 * Copyright (c) 2019 Mr.css
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

package cn.seaboot.commons.core;

import cn.seaboot.commons.lang.P;
import cn.seaboot.commons.lang.Warning;

import java.lang.ref.SoftReference;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;

/**
 * 常用的数字格式化
 *
 * @author Mr.css 2017/8/23上午10:10:38
 */
public class NumberUtils {
    private NumberUtils() {
    }

    /**
     * 判断是不是正确的 区间 表达式：（1, 2）
     *
     * @param range 区间 表达式
     * @return if true
     */
    public static boolean isValidRange(String range) {
        try {
            range = range.trim();
            int idx = range.indexOf(',');
            char mark = range.charAt(0);
            if (mark != '(' && mark != '[') {
                return false;
            }
            mark = range.charAt(range.length() - 1);
            if (mark != ')' && mark != ']') {
                return false;
            }
            String number = range.substring(1, idx).trim();
            Double num1 = null, num2 = null;

            if ("-".equals(number)) {
                num1 = Double.valueOf(number);
            }
            number = range.substring(idx + 1, range.length() - 1).trim();
            if ("-".equals(number)) {
                num2 = Double.valueOf(number);

            }
            if (num1 != null && num2 != null) {
                return num2 >= num1;
            }
            return true;
        } catch (Exception ignore) {
            return false;
        }
    }

    /**
     * 判断一个数值是否在区间内部
     *
     * @param range 区间
     * @param value 数值
     * @return if true
     */
    public static boolean isInRange(String range, Number value) {
        range = range.trim();
        double val = value.doubleValue();
        int idx = range.indexOf(',');
        char mark = range.charAt(0);
        String number = range.substring(1, idx).trim();
        if (!number.equals("-")) {
            double num = Double.parseDouble(number);
            if (mark == '(') {
                if (val <= num) {
                    return false;
                }
            } else if (mark == '[') {
                if (val < num) {
                    return false;
                }
            }
        }
        mark = range.charAt(range.length() - 1);
        number = range.substring(idx + 1, range.length() - 1).trim();
        if (!number.equals("-")) {
            double num = Double.parseDouble(number);
            if (mark == ')') {
                return !(val >= num);
            } else if (mark == ']') {
                return !(val > num);
            }
        }
        return true;
    }

    /**
     * number format by {@link DecimalFormat}, this method is ThreadSafe
     *
     * @param number  number
     * @param pattern pattern
     * @return result
     * @see DecimalFormat
     */
    public static String format(Number number, String pattern) {
        return NumberFormatHolder.formatFor(pattern).format(number);
    }


    /**
     * 是否是数字
     */
    public static boolean isNumber(char ch) {
        return ch > 47 && ch < 58;
    }

    /**
     * 四舍五入，保留2位小数
     *
     * @param number 数字
     * @return BigDecimal
     */
    public static BigDecimal roundHalfUp2(Number number) {
        return roundHalfUp(number, 2);
    }

    /**
     * 四舍五入，保留整数
     *
     * @param number 数字
     * @return BigDecimal
     */
    public static BigDecimal roundHalfUp0(Number number) {
        return roundHalfUp(number, 0);
    }

    /**
     * 四舍五入
     *
     * @param number 数字
     * @param scale  小数位数
     * @return BigDecimal
     */
    public static BigDecimal roundHalfUp(Number number, int scale) {
        return BigDecimal.valueOf(number.doubleValue()).setScale(scale, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 进一法，保留2位小数
     *
     * @param number 数字
     * @return BigDecimal
     */
    public static BigDecimal roundCeiling2(Number number) {
        return roundCeiling(number, 2);
    }

    /**
     * 进一法，保留整数
     *
     * @param number 数字
     * @return BigDecimal
     */
    public static BigDecimal roundCeiling0(Number number) {
        return roundCeiling(number, 0);
    }

    /**
     * 进一法
     *
     * @param number 数字
     * @param scale  小数位数
     * @return BigDecimal
     */
    public static BigDecimal roundCeiling(Number number, int scale) {
        return BigDecimal.valueOf(number.doubleValue()).setScale(scale, BigDecimal.ROUND_CEILING);
    }

    /**
     * 是否是16进制数字
     * <p>
     * Determine whether the given {@code value} String indicates a hex number,
     * i.e. needs to be passed into {@code Integer.decode} instead of
     * {@code Integer.valueOf}, etc.
     */
    private static boolean isHexNumber(String value) {
        int index = value.charAt(0) == '-' ? 1 : 0;
        return value.startsWith("0x", index) || value.startsWith("0X", index) || value.startsWith("#", index);
    }

    /**
     * 是否是科学记数法
     * <p>
     * Determine whether the given {@code value} String indicates a scientific-notation,
     * i.e. needs to be passed into {@code Double.valueOf} instead of
     * {@code Double.valueOf}, etc.
     */
    private static boolean isScientificNotation(String value) {
        return value.indexOf('E') > 0 || value.indexOf('e') > 0;
    }


    /**
     * Decode a {@link java.math.BigInteger} from the supplied {@link String} value.
     * <p>Supports decimal, hex, and octal notation.
     *
     * @see BigInteger#BigInteger(String, int)
     */
    public static BigInteger parseBigInteger(String value) {
        int radix = 10;
        int index = 0;
        boolean negative = false;

        // Handle minus sign, if present.
        if (value.startsWith("-")) {
            negative = true;
            index++;
        }

        // Handle radix specifier, if present.
        if (value.startsWith("0x", index) || value.startsWith("0X", index)) {
            index += 2;
            radix = 16;
        } else if (value.startsWith("#", index)) {
            index++;
            radix = 16;
        } else if (value.startsWith("0", index) && value.length() > 1 + index) {
            index++;
            radix = 8;
        }

        BigInteger result = new BigInteger(value.substring(index), radix);
        return (negative ? result.negate() : result);
    }

    /**
     * Parse the given {@code number} into byte
     *
     * @param number short
     * @return byte value
     */
    public static byte parseByte(String number) {
        if (isHexNumber(number)) {
            return Byte.decode(number);
        } else if (isScientificNotation(number)) {
            return Double.valueOf(number).byteValue();
        } else {
            return Byte.parseByte(number);
        }
    }

    /**
     * Parse the given {@code number} into short
     *
     * @param number short
     * @return int value
     */
    public static short parseShort(String number) {
        if (isHexNumber(number)) {
            return Short.decode(number);
        } else if (isScientificNotation(number)) {
            return Double.valueOf(number).shortValue();
        } else {
            return Short.parseShort(number);
        }
    }

    /**
     * Parse the given {@code number} into int
     *
     * @param number num
     * @return int value
     */
    public static int parseInt(String number) {
        if (isHexNumber(number)) {
            return Integer.decode(number);
        } else if (isScientificNotation(number)) {
            return Double.valueOf(number).intValue();
        } else {
            return Integer.parseInt(number);
        }
    }

    /**
     * Parse the given {@code number} into long
     *
     * @param number num
     * @return long value
     */
    public static long parseLong(String number) {
        if (isHexNumber(number)) {
            return Long.decode(number);
        } else if (isScientificNotation(number)) {
            return Double.valueOf(number).longValue();
        } else {
            return Long.parseLong(number);
        }
    }

    /**
     * Parse the given {@code number} into a {@link Number} instance of the given
     * target class, using the corresponding {@code decode} / {@code valueOf} method.
     * <p>Trims the input {@code String} before attempting to parse the number.
     * <p>Supports numbers in hex format (with leading "0x", "0X", or "#") as well.
     *
     * @param number      the number to convert
     * @param targetClass the target class to convert to
     * @return the converted number
     * @throws IllegalArgumentException if the target class is not supported
     *                                  (i.e. not a standard Number subclass as included in the JDK)
     * @see java.lang.Byte
     * @see java.lang.Short
     * @see java.lang.Integer
     * @see java.lang.Long
     * @see java.math.BigInteger
     * @see java.lang.Float
     * @see java.lang.Double
     * @see java.math.BigDecimal
     */
    @SuppressWarnings(Warning.UNCHECKED)
    public static <T extends Number> T parse(String number, Class<T> targetClass) {
        if (Byte.class == targetClass) {
            return (T) Byte.valueOf(parseByte(number));
        } else if (Short.class == targetClass) {
            return (T) Short.valueOf(parseShort(number));
        } else if (Integer.class == targetClass) {
            return (T) Integer.valueOf(parseInt(number));
        } else if (Long.class == targetClass) {
            return (T) Long.valueOf(parseLong(number));
        } else if (BigInteger.class == targetClass) {
            return (T) (isHexNumber(number) ? parseBigInteger(number) : new BigInteger(number));
        } else if (Float.class == targetClass) {
            return (T) Float.valueOf(number);
        } else if (Double.class == targetClass) {
            return (T) Double.valueOf(number);
        } else if (BigDecimal.class == targetClass || Number.class == targetClass) {
            return (T) new BigDecimal(number);
        }
        throw new IllegalArgumentException("Cannot convert String [" + number + "] to target class [" + targetClass.getName() + "]");
    }

    /**
     * NumberFormat
     *
     * @return NumberFormat
     */
    public static NumberFormat getNumberFormat() {
        NumberFormat nf = NumberFormat.getNumberInstance(P.locale());
        nf.setGroupingUsed(false);
        return nf;
    }

    /**
     * ThreadLocal will remove when thread dead, but, when using thread pool, the thread maybe never dead before system exit,
     * in this way, the threadLocal also never remove.
     * Usually it doesn't cause bugs, because the DecimalFormat always used in one thread. if necessary, you can clear the threadLocal.
     */
    public static void clearThreadLocal() {
        NumberFormatHolder.clearThreadLocal();
    }

    /**
     * A DecimalFormat Holder, use {@link ThreadLocal} and {@link SoftReference}
     */
    static final class NumberFormatHolder {
        private static final ThreadLocal<SoftReference<Map<String, DecimalFormat>>> LOCAL_FORMATS = ThreadLocal.withInitial(
                () -> new SoftReference<>(new HashMap<>())
        );

        static DecimalFormat formatFor(String pattern) {
            SoftReference<Map<String, DecimalFormat>> ref = LOCAL_FORMATS.get();
            Map<String, DecimalFormat> formats = ref.get();
            if (formats == null) {
                formats = new HashMap<>(2);
                LOCAL_FORMATS.set(new SoftReference<>(formats));
            }
            DecimalFormat format = formats.get(pattern);
            if (format == null) {
                format = new DecimalFormat(pattern);
                formats.put(pattern, format);
            }
            return format;
        }

        static void clearThreadLocal() {
            LOCAL_FORMATS.remove();
        }
    }
}
